﻿-- Path of Building
--
-- Class: Calc Breakdown Control
-- Calculation breakdown control used in the Calcs tab
--
local launch, main = ...

local t_insert = table.insert
local m_max = math.max
local m_min = math.min
local m_ceil = math.ceil
local m_floor = math.floor
local m_sin = math.sin
local m_cos = math.cos
local m_pi = math.pi
local band = bit.band

local CalcBreakdownClass = common.NewClass("CalcBreakdown", "Control", "ControlHost", function(self, calcsTab)
	self.Control()
	self.ControlHost()
	self.calcsTab = calcsTab
	self.shown = false
	self.tooltip = common.New("Tooltip")
	self.nodeViewer = common.New("PassiveTreeView")
	self.rangeGuide = NewImageHandle()
	self.rangeGuide:Load("Assets/range_guide.png")
	self.uiOverlay = NewImageHandle()
	self.uiOverlay:Load("Assets/game_ui_small.png")
	self.controls.scrollBar = common.New("ScrollBarControl", {"RIGHT",self,"RIGHT"}, -2, 0, 18, 0, 80, "VERTICAL", true)
end)

function CalcBreakdownClass:IsMouseOver()
	if not self:IsShown() then
		return
	end
	return self:IsMouseInBounds() or self:GetMouseOverControl() 
end

function CalcBreakdownClass:SetBreakdownData(displayData, pinned)
	self.pinned = pinned
	if displayData == self.sourceData then
		return
	end
	self.sourceData = displayData
	self.shown = false
	if not displayData then
		return
	end

	-- Build list of sections
	self.sectionList = wipeTable(self.sectionList)
	for _, sectionData in ipairs(displayData) do
		if self.calcsTab:CheckFlag(sectionData) then
			if sectionData.breakdown then
				self:AddBreakdownSection(sectionData)
			elseif sectionData.modName then
				self:AddModSection(sectionData)
			end
		end
	end
	if #self.sectionList == 0 then
		self.calcsTab:ClearDisplayStat()
		return
	end

	self.shown = true

	-- Determine the size of each section, and the combined content size of the breakdown
	self.contentWidth = 0
	local offset = 2
	for i, section in ipairs(self.sectionList) do
		if section.type == "TEXT" then
			section.width = 0
			for _, line in ipairs(section.lines) do
				section.width = m_max(section.width, DrawStringWidth(section.textSize, "VAR", line) + 8)
			end
			section.height = #section.lines * section.textSize + 4
		elseif section.type == "TABLE" then
			-- This also calculates the width of each column in the table
			section.width = 4
			for _, col in pairs(section.colList) do
				for _, row in pairs(section.rowList) do
					if row[col.key] then
						col.width = m_max(col.width or 0, DrawStringWidth(16, "VAR", col.label) + 6, DrawStringWidth(12, "VAR", row[col.key]) + 6)
					end
				end
				if col.width then
					section.width = section.width + col.width
				end
			end
			if section.label then
				section.width = m_max(section.width, 6 + DrawStringWidth(16, "VAR", section.label..":"))
			end
			section.height = #section.rowList * 14 + 20
			if section.label then
				section.height = section.height + 16
			end
		end
		self.contentWidth = m_max(self.contentWidth, section.width)
		section.offset = offset
		offset = offset + section.height + 8
	end
	self.contentHeight = offset - 6
end

-- Add sections based on the breakdown data generated by the Calcs module
function CalcBreakdownClass:AddBreakdownSection(sectionData)
	local actor = self.calcsTab.input.showMinion and self.calcsTab.calcsEnv.minion or self.calcsTab.calcsEnv.player
	local breakdown
	local ns, name = sectionData.breakdown:match("^(%a+)%.(%a+)$")
	if ns then
		breakdown = actor.breakdown[ns] and actor.breakdown[ns][name]
	else
		breakdown = actor.breakdown[sectionData.breakdown]
	end
	if not breakdown then
		return
	end

	if #breakdown > 0 then
		-- Text lines
		t_insert(self.sectionList, {
			type = "TEXT",
			lines = breakdown,
			textSize = 16
		})
	end

	if breakdown.radius then
		-- Radius visualiser
		t_insert(self.sectionList, {
			type = "RADIUS",
			radius = breakdown.radius,
			width = 8 + 1920/4,
			height = 4 + 1080/4,
		})
	end

	if breakdown.rowList and #breakdown.rowList > 0 then
		-- Generic table
		local section = {
			type = "TABLE",
			label = breakdown.label,
			rowList = breakdown.rowList,
			colList = breakdown.colList,
		}
		t_insert(self.sectionList, section)
	end

	if breakdown.reservations and #breakdown.reservations > 0 then
		-- Reservations table, used for life/mana reservation breakdowns
		local section = {
			type = "TABLE",
			rowList = breakdown.reservations,
			colList = { 
{ label = "【技能】", key = "skillName" },
{ label = "【基础】", key = "base" },
{ label = "【每个】", key = "mult" },
{ label = "【额外 提高/降低】", key = "more" },
{ label = "【提高/降低】", key = "inc" },
{ label = "【保留】", key = "total" },
			}
		}
		t_insert(self.sectionList, section)
	end

	if breakdown.damageTypes and #breakdown.damageTypes > 0 then
		local section = {
			type = "TABLE",
			rowList = breakdown.damageTypes,
			colList = { 
{ label = "【来自】", key = "source", right = true },
{ label = "【基础】", key = "base" },
{ label = "【提高/降低】", key = "inc" },
{ label = "【额外 提高/降低】", key = "more" },
{ label = "【伤害转换】", key = "convSrc" },
{ label = "【总】", key = "total" },
{ label = "【转换】", key = "convDst" },
			}
		}
		t_insert(self.sectionList, section)
	end

	if breakdown.slots and #breakdown.slots > 0 then
		-- Slots table, used for armour/evasion/ES total breakdowns
		local section = { 
			type = "TABLE",
			rowList = breakdown.slots,
			colList = { 
{ label = "【基础】", key = "base", right = true },
{ label = "【提高/降低】", key = "inc" },
{ label = "【额外 提高/降低】", key = "more" },
{ label = "【总】", key = "total", right = true },
{ label = "来源", key = "source" },
{ label = "【名称】", key = "sourceLabel" },
			},
		}
		t_insert(self.sectionList, section)
		for _, row in pairs(section.rowList) do
			if row.item then
				row.sourceLabel = colorCodes[row.item.rarity]..row.item.name
				row.sourceLabelTooltip = function(tooltip)
					self.calcsTab.build.itemsTab:AddItemTooltip(tooltip, row.item, row.source)
				end
			else
				row.sourceLabel = row.sourceName
			end
		end
	end

	if breakdown.modList and #breakdown.modList > 0 then
		-- Provided mod list
		self:AddModSection(sectionData, breakdown.modList)
	end
end

-- Add a table section showing a list of modifiers
function CalcBreakdownClass:AddModSection(sectionData, modList)
	local actor = self.calcsTab.input.showMinion and self.calcsTab.calcsEnv.minion or self.calcsTab.calcsEnv.player
	local build = self.calcsTab.build

	-- Build list of modifiers to display
	local cfg = (sectionData.cfg and actor.mainSkill[sectionData.cfg.."Cfg"] and copyTable(actor.mainSkill[sectionData.cfg.."Cfg"], true)) or { }
	cfg.source = sectionData.modSource
	local rowList
	local modDB = sectionData.enemy and actor.enemy.modDB or actor.modDB
	if modList then	
		rowList = modList
	else
		if type(sectionData.modName) == "table" then
			rowList = modDB:Tabulate(sectionData.modType, cfg, unpack(sectionData.modName))
		else
			rowList = modDB:Tabulate(sectionData.modType, cfg, sectionData.modName)
		end
	end
	if #rowList == 0 then
		return
	end

	-- Create section data
	local section = {
		type = "TABLE",
		label = sectionData.label,
		rowList = rowList,
		colList = { 
{ label = "【值】", key = "displayValue" },
{ label = "【状态】", key = "name" },
{ label = "【技能类型】", key = "flags" },
{ label = "【注意】", key = "tags" },
{ label = "【来源】", key = "source" },
{ label = "【来源名称】", key = "sourceName" },
		},
	}
	t_insert(self.sectionList, section)

	if not modList and not sectionData.modType then
		-- Sort modifiers by type
		for i, row in ipairs(rowList) do
			row.index = i
		end
		table.sort(rowList, function(a, b)
			if a.mod.type == b.mod.type then
				return a.index < b.index
			else
				return a.mod.type < b.mod.type
			end
		end)
	end

	local sourceTotals = { }
	if not modList and not sectionData.modSource then
		-- Build list of totals from each modifier source
		local types = { }
		local typeList = { }
		for i, row in ipairs(rowList) do
			-- Find all the modifier types and source types that are present in the modifier lsit
			if not types[row.mod.type] then
				types[row.mod.type] = true
				t_insert(typeList, row.mod.type)
			end
			local sourceType = row.mod.source:match("[^:]+")
			if not sourceTotals[sourceType] then
				sourceTotals[sourceType] = { }
			end	
		end
		for sourceType, lines in pairs(sourceTotals) do
			cfg.source = sourceType
			for _, modType in ipairs(typeList) do
				if type(sectionData.modName) == "table" then
					-- Multiple stats, show each separately
					for _, modName in ipairs(sectionData.modName) do
						local total = modDB:Sum(modType, cfg, modName)
						if modType == "MORE" then
							total = round((total - 1) * 100)
						end
						if total and total ~= 0 then
							t_insert(lines, self:FormatModValue(total, modType) .. " " .. modName:gsub("(%l)(%u)","%1 %2"))
						end
					end
				else
					local total = modDB:Sum(modType, cfg, sectionData.modName)
					if modType == "MORE" then
						total = round((total - 1) * 100)
					end
					if total and total ~= 0 then
						t_insert(lines, self:FormatModValue(total, modType))
					end
				end
			end
		end
	end

	-- Process modifier data
	for _, row in ipairs(rowList) do
		if not sectionData.modType then
			-- No modifier type specified, so format the value to convey type
			row.displayValue = self:FormatModValue(row.value, row.mod.type)
		else
			section.colList[1].right = true
			row.displayValue = formatRound(row.value, 2)
		end
		if modList or type(sectionData.modName) == "table" then
			-- Multiple stat names specified, add this modifier's stat to the table
			row.name = self:FormatModName(row.mod.name)
		end
		local sourceType = row.mod.source:match("[^:]+")
		if not modList and not sectionData.modSource then
			-- No modifier source specified, add the source type to the table
			row.source = sourceType
			row.sourceTooltip = function(tooltip)
				tooltip:AddLine(16, "Total from "..sourceType..":")
				for _, line in ipairs(sourceTotals[sourceType]) do
					tooltip:AddLine(14, line)
				end
			end
		end
		if sourceType == "Item" then
			-- Modifier is from an item, add item name and tooltip
			local itemId = row.mod.source:match("Item:(%d+):.+")
			local item = build.itemsTab.items[tonumber(itemId)]
			if item then
				row.sourceName = colorCodes[item.rarity]..item.name
				row.sourceNameTooltip = function(tooltip)
					build.itemsTab:AddItemTooltip(tooltip, item, row.mod.sourceSlot)
				end
			end
		elseif sourceType == "Tree" then
			-- Modifier is from a passive node, add node name, and add node ID (used to show node location)
			local nodeId = row.mod.source:match("Tree:(%d+)")
			if nodeId then
				local node = build.spec.nodes[tonumber(nodeId)]
				row.sourceName = node.dn
				row.sourceNameNode = node
			end
		elseif sourceType == "Skill" then
			-- Extract skill name
			row.sourceName = build.data.skills[row.mod.source:match("Skill:(.+)")].name
		end
		if row.mod.flags ~= 0 or row.mod.keywordFlags ~= 0 then
			-- Combine, sort and format modifier flags
			local flagNames = { }
			for flags, src in pairs({[row.mod.flags] = ModFlag, [row.mod.keywordFlags] = KeywordFlag}) do
				for name, val in pairs(src) do
					if band(flags, val) == val then
						t_insert(flagNames, name)
					end
				end
			end
			table.sort(flagNames)
			row.flags = table.concat(flagNames, ", ")
		end
		row.tags = nil
		if row.mod[1] then
			-- Format modifier tags
			local baseVal = type(row.mod.value) == "number" and (self:FormatModBase(row.mod, row.mod.value) .. "")
			for _, tag in ipairs(row.mod) do
				local desc
				if tag.type == "Condition" or tag.type == "ActorCondition" then
					desc = (tag.actor and (tag.actor:sub(1,1):upper()..tag.actor:sub(2).." ") or "").."Condition: "..(tag.neg and "Not " or "")..self:FormatVarNameOrList(tag.var, tag.varList)
				elseif tag.type == "Multiplier" then
					local base = tag.base and (self:FormatModBase(row.mod, tag.base).."+ "..math.abs(row.mod.value).." ") or baseVal
					desc = base.."per "..(tag.div and (tag.div.." ") or "")..self:FormatVarNameOrList(tag.var, tag.varList)
					baseVal = ""
				elseif tag.type == "PerStat" then
					local base = tag.base and (self:FormatModBase(row.mod, tag.base).."+ "..math.abs(row.mod.value).." ") or baseVal
					desc = base.."per "..(tag.div or 1).." "..self:FormatVarNameOrList(tag.stat, tag.statList)
					baseVal = ""
				elseif tag.type == "MultiplierThreshold" or tag.type == "StatThreshold" then
					desc = "If "..self:FormatVarNameOrList(tag.var or tag.stat, tag.varList or tag.statList)..(tag.upper and " <= " or " >= ")..(tag.threshold or self:FormatModName(tag.thresholdVar or tag.thresholdStat))
				elseif tag.type == "SkillName" then
					desc = "Skill: "..tag.skillName
				elseif tag.type == "SkillId" then
					desc = "Skill: "..build.data.skills[tag.skillId].name
				elseif tag.type == "SkillType" then
					for name, type in pairs(SkillType) do
						if type == tag.skillType then
							desc = "Skill type: "..(tag.neg and "Not " or "")..self:FormatModName(name)
							break
						end
					end
					if not desc then
						desc = "Skill type: "..(tag.neg and "Not " or "").."?"
					end
				elseif tag.type == "SlotNumber" then
					desc = "When in slot #"..tag.num
				elseif tag.type == "GlobalEffect" then
					desc = tag.effectType
				else
					desc = self:FormatModName(tag.type)
				end
				if desc then
					row.tags = (row.tags and row.tags .. ", " or "") .. desc
				end
			end
		end
	end
end

function CalcBreakdownClass:FormatModName(modName)
	return modName:gsub("([%l%d]:?)(%u)","%1 %2"):gsub("(%l)(%d)","%1 %2")
end

function CalcBreakdownClass:FormatVarNameOrList(var, varList)
	return var and self:FormatModName(var) or table.concat(varList, "/")
end

function CalcBreakdownClass:FormatModBase(mod, base)
	return mod.type == "BASE" and string.format("%+g ", math.abs(base)) or math.abs(base).."% "
end

function CalcBreakdownClass:FormatModValue(value, modType)
	if modType == "BASE" then
		return string.format("%+g base", value)
	elseif modType == "INC" then
		if value >= 0 then
			return value.."% increased"
		else
			return -value.."% reduced"
		end
	elseif modType == "MORE" then
		if value >= 0 then
			return value.."% more"
		else
			return -value.."% less"
		end
	elseif modType == "OVERRIDE" then
		return "Override: "..value
	elseif modType == "FLAG" then
		return value and "True" or "False"
	else
		return value		
	end
end

function CalcBreakdownClass:DrawBreakdownTable(viewPort, x, y, section)
	local cursorX, cursorY = GetCursorPos()
	if section.label then
		-- Draw table lable if able
		DrawString(x + 2, y, "LEFT", 16, "VAR", "^7"..section.label..":")
		y = y + 16
	end
	local colX = x + 4
	for index, col in ipairs(section.colList) do
		if col.width then
			-- Column is present, draw the separator and label
			col.x = colX
			if index > 1 then
				-- Skip the separator for the first column
				SetDrawColor(0.5, 0.5, 0.5)
				DrawImage(nil, colX - 2, y, 1, section.label and section.height - 16 or section.height)
			end
			SetDrawColor(1, 1, 1)
			DrawString(colX, y + 2, "LEFT", 16, "VAR", col.label)
			colX = colX + col.width
		end
	end
	local rowY = y + 20
	for _, row in ipairs(section.rowList) do
		-- Draw row separator
		SetDrawColor(0.5, 0.5, 0.5)
		DrawImage(nil, x + 2, rowY - 1, section.width - 4, 1)
		for _, col in ipairs(section.colList) do
			if col.width and row[col.key] then
				-- This row has an entry for this column, draw it
				if col.right then
					DrawString(col.x + col.width - 4, rowY + 1, "RIGHT_X", 12, "VAR", "^7"..row[col.key])
				else
					DrawString(col.x, rowY + 1, "LEFT", 12, "VAR", "^7"..row[col.key])
				end
				local ttFunc = row[col.key.."Tooltip"]
				local ttNode = row[col.key.."Node"]
				if (ttFunc or ttNode) and cursorY >= viewPort.y + 2 and cursorY < viewPort.y + viewPort.height - 2 and cursorX >= col.x and cursorY >= rowY and cursorX < col.x + col.width and cursorY < rowY + 14 then
					-- Mouse is over the cell, draw highlighting lines and show the tooltip/node location
					SetDrawLayer(nil, 15)
					SetDrawColor(0, 1, 0)
					DrawImage(nil, col.x - 2, rowY - 1, col.width, 1)
					DrawImage(nil, col.x - 2, rowY + 13, col.width, 1)
					if ttFunc then
						self.tooltip:Clear()
						ttFunc(self.tooltip)
						self.tooltip:Draw(col.x, rowY, col.width, 12, viewPort)
					elseif ttNode then
						local viewerX = col.x + col.width + 5
						if viewPort.x + viewPort.width < viewerX + 304 then
							viewerX = col.x - 309
						end
						local viewerY = m_min(rowY, viewPort.y + viewPort.height - 304)
						SetDrawColor(1, 1, 1)
						DrawImage(nil, viewerX, viewerY, 304, 304)
						local viewer = self.nodeViewer
						viewer.zoom = 5
						viewer.zoomX = -ttNode.x / 11.85
						viewer.zoomY = -ttNode.y / 11.85
						SetViewport(viewerX + 2, viewerY + 2, 300, 300)
						viewer:Draw(self.calcsTab.build, { x = 0, y = 0, width = 300, height = 300 }, { })
						SetDrawLayer(nil, 30)
						SetDrawColor(1, 0, 0)
						DrawImage(viewer.highlightRing, 135, 135, 30, 30)
						SetViewport()
					end
					SetDrawLayer(nil, 10)
				end
			end
		end
		rowY = rowY + 14
	end
end

function CalcBreakdownClass:DrawRadiusVisual(x, y, width, height, radius)
	SetDrawColor(0.75, 0.75, 0.75)
	DrawImage(self.rangeGuide, x, y, width, height)
	--SetDrawColor(0, 0, 0)
	--DrawImage(nil, x, y, width, height)
	--[[SetDrawColor(0.5, 0.5, 0.75)
	for r = 10, 130, 20 do
		main:RenderRing(x, y, width, height, 0, 0, r, 3)
	end
	SetDrawColor(1, 1, 1)
	for r = 20, 120, 20 do
		main:RenderRing(x, y, width, height, 0, 0, r, 3)
	end
	main:RenderCircle(x, y, width, height, 0, 0, 2)]]
	SetDrawColor(0.5, 1, 0.5, 0.33)
	main:RenderCircle(x, y, width, height, 0, 0, radius)
	--[[SetDrawColor(1, 0.5, 0.5, 0.33)
	if not self.foo1 then
		self.foo1, self.foo2 = 0, 0
	end
	if IsKeyDown("LEFT") then
		self.foo1 = self.foo1 - 0.3
	elseif IsKeyDown("RIGHT") then
		self.foo1 = self.foo1 + 0.3
	end
	if IsKeyDown("UP") then
		self.foo2 = self.foo2 + 0.3
	elseif IsKeyDown("DOWN") then
		self.foo2 = self.foo2 - 0.3
	end
	main:RenderCircle(x, y, width, height, self.foo1, self.foo2, 30)]]
	SetDrawColor(1, 1, 1)
	DrawImage(self.uiOverlay, x, y, width, height)
end

function CalcBreakdownClass:Draw(viewPort)
	local sourceData = self.sourceData
	local scrollBar = self.controls.scrollBar
	local width = self.contentWidth
	local height = self.contentHeight
	if self.contentHeight > viewPort.height then
		-- Content won't fit the screen height, so set the scrollbar
		width = self.contentWidth + scrollBar.width
		height = viewPort.height
		scrollBar.height = height - 4
		scrollBar:SetContentDimension(self.contentHeight - 4, viewPort.height - 4)
	else
		scrollBar:SetContentDimension(0, 0)
	end
	self.width = width
	self.height = height
	-- Calculate position based on the source cell
	local x = sourceData.x + sourceData.width + 5
	local y = m_min(sourceData.y, viewPort.y + viewPort.height - height)
	if x + width > viewPort.x + viewPort.width then
		x = m_max(viewPort.x, sourceData.x - 5 - width)
	end
	self.x = x
	self.y = y
	-- Draw background
	SetDrawLayer(nil, 10)
	SetDrawColor(0, 0, 0, 0.9)
	DrawImage(nil, x + 2, y + 2, width - 4, height - 4)
	-- Draw border (this is put in sub layer 11 so it draws over the contents, in case they don't fit the screen)
	SetDrawLayer(nil, 11)
	if self.pinned then
		SetDrawColor(0.25, 1, 0.25)
	else
		SetDrawColor(0.33, 0.66, 0.33)
	end
	DrawImage(nil, x, y, width, 2)
	DrawImage(nil, x, y + height - 2, width, 2)
	DrawImage(nil, x, y, 2, height)
	DrawImage(nil, x + width - 2, y, 2, height)
	SetDrawLayer(nil, 10)
	self:DrawControls(viewPort)
	-- Draw the sections
	y = y - scrollBar.offset
	for i, section in ipairs(self.sectionList) do
		local sectionY = y + section.offset
		if section.type == "TEXT" then
			local lineY = sectionY + 2
			for i, line in ipairs(section.lines) do
				SetDrawColor(1, 1, 1)
				DrawString(x + 4, lineY, "LEFT", section.textSize, "VAR", line)
				lineY = lineY + section.textSize
			end
		elseif section.type == "TABLE" then
			self:DrawBreakdownTable(viewPort, x, sectionY, section)
		elseif section.type == "RADIUS" then
			SetDrawColor(1, 1, 1)
			DrawImage(nil, x + 2, sectionY, section.width - 4, section.height)
			self:DrawRadiusVisual(x + 4, sectionY + 2, section.width - 8, section.height - 4, section.radius)
		end
	end
	SetDrawLayer(nil, 0)
end

function CalcBreakdownClass:OnKeyDown(key, doubleClick)
	if not self:IsShown() or not self:IsEnabled() then
		return
	end
	local mOverControl = self:GetMouseOverControl()
	if mOverControl and mOverControl.OnKeyDown then
		return mOverControl:OnKeyDown(key)
	end
	local mOver = self:IsMouseOver()
	if key:match("BUTTON") then
		if not mOver then
			-- Mouse click outside the control, hide the breakdown
			self.calcsTab:ClearDisplayStat()
			self.shown = false
			return
		end
	end
	return self
end

function CalcBreakdownClass:OnKeyUp(key)
	if not self:IsShown() or not self:IsEnabled() then
		return
	end
	if key == "WHEELDOWN" then
		self.controls.scrollBar:Scroll(1)
	elseif key == "WHEELUP" then
		self.controls.scrollBar:Scroll(-1)
	end
	return self
end
